package com.michaelbulava.TFS.ui.dialogs.connect;


import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.DialogWrapper;
import com.michaelbulava.TFS.Messages;
import com.michaelbulava.TFS.log.FileLogger;
import com.michaelbulava.TFS.ui.config.ClientConnectionAdvisor;
import com.michaelbulava.TFS.ui.controls.browser.FullFeaturedBrowser;
import com.microsoft.tfs.core.config.ConnectionInstanceData;
import com.microsoft.tfs.core.httpclient.*;
import com.microsoft.tfs.core.httpclient.cookie.CookiePolicy;
import com.microsoft.tfs.core.httpclient.cookie.CookieSpec;
import com.microsoft.tfs.core.httpclient.methods.PostMethod;
import com.microsoft.tfs.core.util.URIUtils;
import com.microsoft.tfs.core.ws.runtime.exceptions.FederatedAuthException;
import com.microsoft.tfs.util.Check;
import com.microsoft.tfs.util.GUID;
import com.microsoft.tfs.util.StringHelpers;
import com.microsoft.tfs.util.TypesafeEnum;
import com.microsoft.tfs.util.listeners.SingleListenerFacade;
import org.jetbrains.annotations.Nullable;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

import javax.swing.*;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import java.awt.*;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.awt.event.WindowStateListener;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.InvocationTargetException;
import java.net.URI;
import java.net.URLDecoder;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;

/**
 * Created by mbulava on 6/29/2015.
 */
public class ACSCredentialsDialog extends DialogWrapper implements CredentialsCompleteDialog {
    private static final Logger log = FileLogger.getInstance(ACSCredentialsDialog.class);

    private static final int MINIMUM_SWT_VERSION = 3500;
    private static final String ENABLE_BROWSER_AUTH_PROPERTY_NAME = "com.microsoft.tfs.client.common.ui.dialogs.connect.enablebrowserauth";
    private static Boolean browserAvailable = null;
    private static int browserAvailableForStyle;
    private static final String HTML_RESOURCE_PATH = "resources";
    private static final String LOADING_HTML_RESOURCE_NAME = "acscredentialsdialog.html";
    private final URI serverURI;
    private final URI serverSigninURL;
    private final FederatedAuthException exception;
    private static final String ACS_SCHEME = "https";
    private static final String ACS_DOMAIN = ".accesscontrol.windows.net";
    private static final String ACS_PATH = "/v2/wsfederation";
    private static final String ACS_QUERY = "wa=wsignin1.0";
    private final List<Cookie> cookies = new ArrayList();
    private Boolean useAuthenticationProviderPage = Boolean.TRUE;
    private FullFeaturedBrowser browser;
    private HttpClient httpClient;
    private final SingleListenerFacade credentialsCompleteListeners = new SingleListenerFacade(CredentialsCompleteListener.class);

    private boolean credentialCompleteCalled = false;

    public ACSCredentialsDialog(Project project, URI serverURI, URI serverSigninURL, FederatedAuthException exception) {
        super(project, true, IdeModalityType.IDE);
        init();
        Check.notNull(serverURI, "serverURI");
        Check.notNull(serverSigninURL, "serverSigninURL");
        Check.notNull(exception, "exception");

        this.serverURI = serverURI;
        this.serverSigninURL = getSignInURI(serverSigninURL);
        this.exception = exception;

        log.info("serverURI = " + serverURI);
        log.info("serverSigninURL = " + this.serverSigninURL);
        log.info("exception: " + exception.getMessage());
        log.info("    FedAuthIssuer = " + exception.getFedAuthIssuer());
        log.info("    FedAuthRealm = " + exception.getFedAuthRealm());
        log.info("    ServerURI = " + exception.getServerURI());
        log.info("    Credentials = " + (exception.getCredentials() != null ? exception.getCredentials() : "null"));
        log.info("    Mechanisms = " + StringHelpers.join(exception.getMechanisms(), ", "));

        browser.setJavaEnabled(true);
        browser.addLocationListener(new FullFeaturedBrowser.LocationListener() {
            @Override
            public void changing(FullFeaturedBrowser.LocationEvent event) {
                URI locationURI;
                try {
                    locationURI = URIUtils.newURI(event.location);
                } catch (IllegalArgumentException e) {
                    log.warn(MessageFormat.format("Server redirected to an unparseable URL: {0}",
                            new Object[]{event.location}), e);
                    return;
                }
                ACSConfigurationResult configurationResult = ACSConfigurationResult.CONTINUE;
                try {
                    configurationResult = configureAuthenticationFromCurrentPage(locationURI);
                } catch (Throwable e) {
                    log.error("Could not process ACS authentication", e);
                    configurationResult = ACSConfigurationResult.FAILURE;
                }
                if (ACSConfigurationResult.CONTINUE.equals(configurationResult)) {
                    return;
                }
                ACSCredentialsDialog.this.myClose(configurationResult.getValue());
//                ACSConfigurationResult.SUCCESS.equals(configurationResult) ?
//                        DialogWrapper.OK_EXIT_CODE : DialogWrapper.CANCEL_EXIT_CODE);
                if (ACSConfigurationResult.FAILURE.equals(configurationResult)) {
                    Messages.showErrorDialog(ACSCredentialsDialog.this.browser,
                            Messages.getString("ACSCredentialsDialog.ACSFailedErrorMessage"),
                            Messages.getString("ACSCredentialsDialog.ACSFailedErrorTitle"));
                }
                event.doit = false;
            }

            @Override
            public void changed(FullFeaturedBrowser.LocationEvent event) {
                log.warn("The location changed to the URL: " + event.location);
                String message;
                if (event.location.equalsIgnoreCase(ACSCredentialsDialog.this.serverSigninURL.toString())) {
                    if ((ACSCredentialsDialog.this.exception.getCredentials() != null) &&
                            ((ACSCredentialsDialog.this.exception.getCredentials() instanceof CookieCredentials))) {
                        message = MessageFormat.format(Messages.getString("ACSCredentialsDialog.UnauthorizedErrorMessageFormat"),
                                new Object[]{ACSCredentialsDialog.this.serverURI.toString()});
                    } else {
                        message = Messages.getString("ACSCredentialsDialog.SignInMessage");
                    }
                    //ACSCredentialsDialog.this.locationText.setText(message);
                    log.warn(message);
                } else {
                    message = Messages.getString("ACSCredentialsDialog.DoneMessageText");
                }
                //ACSCredentialsDialog.this.locationText.setText(message);
                log.warn(message);
            }
        });
        browser.setOnTitleChangeListener(new FullFeaturedBrowser.OnTitleChangeListener() {
            @Override
            public void titleChanged(String title) {
                setTitle(title);
            }
        });
        browser.navigate(this.serverSigninURL.toString());

        //This should ensure that credentialsComplete is always called, before the form closes.
        getWindow().setSize(392, 478);
        getWindow().addWindowStateListener(new WindowStateListener() {
            @Override
            public void windowStateChanged(WindowEvent e) {
                if (e.getNewState() == WindowEvent.WINDOW_CLOSING && !credentialCompleteCalled) {
                    ((CredentialsCompleteListener) credentialsCompleteListeners.getListener()).credentialsComplete();
                    credentialCompleteCalled  = true;
                }
            }
        });
        getWindow().addWindowListener(new WindowListener() {
            @Override
            public void windowOpened(WindowEvent e) {

            }

            @Override
            public void windowClosing(WindowEvent e) {
                if (!credentialCompleteCalled)
                    ((CredentialsCompleteListener) credentialsCompleteListeners.getListener()).credentialsComplete();
                credentialCompleteCalled  = true;
            }

            @Override
            public void windowClosed(WindowEvent e) {

            }

            @Override
            public void windowIconified(WindowEvent e) {

            }

            @Override
            public void windowDeiconified(WindowEvent e) {

            }

            @Override
            public void windowActivated(WindowEvent e) {

            }

            @Override
            public void windowDeactivated(WindowEvent e) {

            }
        });
    }


    protected void myClose(final int exitCode){
        try {
            EventQueue.invokeAndWait(new Runnable() {
                @Override
                public void run() {
                    close(exitCode);
                    if (!credentialCompleteCalled)
                        ((CredentialsCompleteListener) credentialsCompleteListeners.getListener()).credentialsComplete();
                    credentialCompleteCalled  = true;
                }
            });
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }

    }

    @Override
    protected Action[] createActions() {
        return new Action[0];
    }

    private URI getSignInURI(URI serverSigninURL) {
        String query = serverSigninURL.getQuery();
////        if (query.indexOf("protocol=") < 0) {
////            query = query + "&protocol=javascriptnotify";
////        }
        if (query.indexOf("force=") < 0) {
            query = query + "&force=1";
        }
        if (query.indexOf("compact=") < 0) {
            query = query + "&compact=1";
        }
        try {
            return new URI(serverSigninURL.getScheme(), serverSigninURL.getUserInfo(), serverSigninURL.getHost(), serverSigninURL.getPort(), serverSigninURL.getPath(), query, serverSigninURL.getFragment());
        } catch (Exception e) {}
        return serverSigninURL;
    }


    @Override
    public void addCredentialsCompleteListener(CredentialsCompleteListener listener) {
        this.credentialsCompleteListeners.addListener(listener);
    }

    public static boolean isAvailable(){
        return true;
    }

    @Override
    protected void init(){
        super.init();
    }

    private boolean useAuthenticationProviderPage() {
        if (this.useAuthenticationProviderPage == null)
        {
            this.useAuthenticationProviderPage = Boolean.FALSE;
//            if (Platform.isCurrentPlatform(Platform.MAC_OS_X)) {
//                try
//                {
//                    String userAgent = (String)this.browser.evaluate("return navigator.userAgent;");
//                    if (userAgent.contains("AppleWebKit/534.56.5 ")) {
//                        this.useAuthenticationProviderPage = Boolean.TRUE;
//                    }
//                }
//                catch (Exception e)
//                {
//                    log.warn("Could not determine Webkit browser version", e);
//                }
//            }
        }
        return this.useAuthenticationProviderPage.booleanValue();
    }

    private ACSConfigurationResult configureAuthenticationFromCurrentPage(URI locationURI) throws Exception{
        if ((useAuthenticationProviderPage()) && (ACS_SCHEME.equalsIgnoreCase(locationURI.getScheme())) &&
                (locationURI.getHost() != null) &&
                (locationURI.getHost().toLowerCase().endsWith(ACS_DOMAIN)) &&
                (ACS_PATH.equalsIgnoreCase(locationURI.getPath())) &&
                (ACS_QUERY.equalsIgnoreCase(locationURI.getQuery()))) {
            return configureAuthenticationFromAuthProviderPage();
        }
        if ((locationURI.getHost().equalsIgnoreCase(this.serverSigninURL.getHost())) &&
                (locationURI.getPath().equalsIgnoreCase(this.serverSigninURL.getPath()))) {
            String[] queryPairs = locationURI.getQuery().split("&");
            for (String pair : queryPairs) {
                String[] keyValue = pair.split("=", 2);
                String key;
                String value;
                try {
                    key = URLDecoder.decode(keyValue[0], "UTF-8");
                    value = URLDecoder.decode(keyValue[1], "UTF-8");
                } catch (UnsupportedEncodingException e) {
                    log.warn("Could not decode location URI query parameters as UTF-8", e);
                    break;
                }
                if (("complete".equalsIgnoreCase(key)) && ("1".equals(value))) {
                    return configureAuthenticationFromACSPage();
                }
            }
        }
        return ACSConfigurationResult.CONTINUE;
    }

    private ACSConfigurationResult configureAuthenticationFromAuthProviderPage()
            throws Exception {
        Object hasSwtForm = this.browser.evaluate("if(document != null && document.forms != null && document.forms.length == 1) { return true; } return false;");
        if (!Boolean.TRUE.equals(hasSwtForm)) {
            throw new Exception("Document does not contain ACS form data");
        }
        String postLocation = (String)this.browser.evaluate("return document.forms[0].action;");
        if (postLocation == null) {
            throw new Exception("ACS form does not have an action");
        }
        int elementCount = 0;
        Object elementCountObj = this.browser.evaluate("return document.forms[0].elements.length;");
        if ((elementCountObj instanceof Integer)) {
            elementCount = ((Integer)elementCountObj).intValue();
        } else if ((elementCountObj instanceof Double)) {
            elementCount = (int)((Double)elementCountObj).doubleValue();
        } else {
            throw new Exception(MessageFormat.format("Could not deserialize ACS form element length ({0})", new Object[] { elementCountObj }));
        }
        if (elementCount == 0) {
            throw new Exception("No ACS form elements");
        }
        NameValuePair[] postParameters = new NameValuePair[elementCount];
        for (int i = 0; i < elementCount; i++) {
            postParameters[i] = new NameValuePair((String)this.browser.evaluate(MessageFormat.format("return document.forms[0].elements[{0}].name;",
                        new Object[] { Integer.toString(i) })),
                    (String)this.browser.evaluate(MessageFormat.format("return document.forms[0].elements[{0}].value;",
                            new Object[] { Integer.toString(i) })));
        }
        return configureAuthenticationFromAuthProvider(postLocation, postParameters);
    }

    private ACSConfigurationResult configureAuthenticationFromAuthProvider(String target, NameValuePair[] formInput)
            throws Exception {
        URI targetURI;
        try {
            targetURI = new URI(target);
        } catch (Exception e) {
            log.warn(MessageFormat.format("ACS form action is not parseable: {0}", new Object[] { target }), e);
            throw new Exception("ACS form action is not parseable");
        }
        if ((!"https".equalsIgnoreCase(targetURI.getScheme())) || (targetURI.getHost() == null) || (!targetURI.getHost().toLowerCase().endsWith(".accesscontrol.windows.net")) || (!"/v2/wsfederation".equalsIgnoreCase(targetURI.getPath())) || (!"wa=wsignin1.0".equalsIgnoreCase(targetURI.getQuery()))) {
            throw new Exception(MessageFormat.format("ACS form location is not in the domain: {0}", new Object[] { ".accesscontrol.windows.net" }));
        }
        HttpClient httpClient = getHttpClient();
        PostMethod postMethod = new PostMethod(target);






        postMethod.getParams().setCookiePolicy("ignoreCookies");
        postMethod.setFollowRedirects(false);
        postMethod.setDoAuthentication(false);
        postMethod.getParams().setContentCharset("UTF-8");
        for (int i = 0; i < formInput.length; i++) {
            postMethod.addParameter(formInput[i]);
        }
        int status = httpClient.executeMethod(postMethod);
        if (status != 200)
        {
            String message = MessageFormat.format("ACS authentication did not return success: {0}", new Object[] { Integer.toString(status) });

            throw new Exception(message);
        }
        SAXParser acsResultParser = SAXParserFactory.newInstance().newSAXParser();
        ACSResultHandler acsResultHandler = new ACSResultHandler();
        acsResultParser.parse(postMethod.getResponseBodyAsStream(), acsResultHandler);

        String finalTarget = acsResultHandler.getFormAction();
        NameValuePair[] finalParameters = acsResultHandler.getFormInputs();

        return configureAuthenticationFromACS(finalTarget, finalParameters);
    }

    private ACSConfigurationResult configureAuthenticationFromACSPage()
            throws Exception {
        Object hasSwtForm = this.browser.evaluate("if(document != null && document.hiddenform != null) { return true; } return false;");
        if (!Boolean.TRUE.equals(hasSwtForm)) {
            throw new Exception("Document does not contain ACS form data");
        }
        String postLocation = (String)this.browser.evaluate("return document.hiddenform.action;");
        if (postLocation == null) {
            throw new Exception("ACS form does not have an action");
        }
        int elementCount = 0;
        Object elementCountObj = this.browser.evaluate("return document.hiddenform.elements.length;");
        if ((elementCountObj instanceof Integer)) {
            elementCount = ((Integer)elementCountObj).intValue();
        } else if ((elementCountObj instanceof Double)) {
            elementCount = (int)((Double)elementCountObj).doubleValue();
        } else {
            throw new Exception(MessageFormat.format("Could not deserialize ACS form element length ({0})", new Object[] { elementCountObj }));
        }
        if (elementCount == 0) {
            throw new Exception("No ACS form elements");
        }
        NameValuePair[] postParameters = new NameValuePair[elementCount];
        for (int i = 0; i < elementCount; i++) {
            postParameters[i] = new NameValuePair((String)this.browser.evaluate(MessageFormat.format("return document.hiddenform.elements[{0}].name;", new Object[] { Integer.toString(i) })), (String)this.browser.evaluate(MessageFormat.format("return document.hiddenform.elements[{0}].value;", new Object[] { Integer.toString(i) })));
        }
        return configureAuthenticationFromACS(postLocation, postParameters);
    }

    private ACSConfigurationResult configureAuthenticationFromACS(String target, NameValuePair[] formInput)
            throws Exception
    {
        URI targetURI;
        try
        {
            targetURI = new URI(target);
        }
        catch (Exception e)
        {
            log.warn(MessageFormat.format("ACS form action is not parseable: {0}", new Object[] { target }), e);
            throw new Exception("ACS form action is not parseable");
        }
        if (!targetURI.getHost().equalsIgnoreCase(this.serverSigninURL.getHost())) {
            throw new Exception("ACS form location does not match initial target");
        }
        HttpClient httpClient = getHttpClient();
        PostMethod postMethod = new PostMethod(target);






        postMethod.getParams().setCookiePolicy("ignoreCookies");
        postMethod.setFollowRedirects(false);
        postMethod.setDoAuthentication(false);

        postMethod.getParams().setContentCharset("UTF-8");
        for (int i = 0; i < formInput.length; i++) {
            postMethod.addParameter(formInput[i]);
        }
        int status = httpClient.executeMethod(postMethod);

        ArrayList<Cookie> fedAuthCookies = new ArrayList();
        if (status == 302 || status == 200)
        {
            Header[] cookieHeaders = postMethod.getResponseHeaders("Set-Cookie");
            if (cookieHeaders.length == 0) {
                throw new Exception("Team Foundation Server did not return FedAuth tokens");
            }
            CookieSpec cookieParser = CookiePolicy.getCookieSpec("rfc2109");
            for (Header cookieHeader : cookieHeaders) {
                try
                {
                    Cookie[] cookies = cookieParser.parse(targetURI, cookieHeader.getValue());
                    for (Cookie cookie : cookies) {
                        if (/*cookie.getName().startsWith("FedAuth")&&*/
                                !fedAuthCookies.contains(cookie)){
                            fedAuthCookies.add(cookie);
                        }

                    }
                }
                catch (Exception e)
                {
                    log.warn(MessageFormat.format("Could not parse authentication cookie {0}", new Object[] { cookieHeader.getValue() }), e);
                }
            }
        }
        else
        {
            String logMessage = "ACS authentication to TFS failed.";
            String exceptionMessage = MessageFormat.format("Received unexpected HTTP status code during authentication: {0}.", new Object[] { Integer.toString(status) });
            if (status == 500) {
                try
                {
                    Header[] headers = postMethod.getResponseHeaders("X-TFS-ServiceError");
                    for (Header header : headers) {
                        logMessage = logMessage + MessageFormat.format("  TFS error was: {0}", new Object[] { URLDecoder.decode(header.getValue(), "UTF-8") });
                    }
                }
                catch (Exception e) {}
            }
            log.warn(logMessage);
            throw new Exception(exceptionMessage);
        }
        this.cookies.clear();
        this.cookies.addAll(fedAuthCookies);

        return ACSConfigurationResult.SUCCESS;
    }

    private HttpClient getHttpClient() {
        if (this.httpClient == null) {
            ConnectionInstanceData instanceData = new ConnectionInstanceData(this.serverURI, GUID.newGUID());

            ClientConnectionAdvisor connectionAdvisor = new ClientConnectionAdvisor();
            this.httpClient = connectionAdvisor.getHTTPClientFactory(instanceData).newHTTPClient();
        }
        return this.httpClient;
    }

    public List<Cookie> getFederatedAuthenticationCookies() {
        return this.cookies;
    }

    @Override
    public Credentials getCredentials() {
        return new CookieCredentials((Cookie[])this.cookies.toArray(new Cookie[this.cookies.size()]));
    }

    @Nullable
    @Override
    protected JComponent createCenterPanel() {
        if (browser == null){
            browser = new FullFeaturedBrowser();
        }

        return browser;
    }

    @Override
    public String getTitle(){
        return MessageFormat.format(Messages.getString("ACSCredentialsDialog.DialogTitleFormat"),
                new Object[]{this.serverURI.getHost()});
    }



    private static class ACSConfigurationResult
            extends TypesafeEnum {
        public static final ACSConfigurationResult CONTINUE = new ACSConfigurationResult(DialogWrapper.NEXT_USER_EXIT_CODE);
        public static final ACSConfigurationResult FAILURE = new ACSConfigurationResult(DialogWrapper.CANCEL_EXIT_CODE);
        public static final ACSConfigurationResult SUCCESS = new ACSConfigurationResult(DialogWrapper.OK_EXIT_CODE);

        private ACSConfigurationResult(int value) {
            super(value);
        }
    }

    private static class ACSResultHandler
            extends DefaultHandler
    {
        private final List<String> elementHierarchy = new ArrayList();
        private boolean inForm = false;
        private String formAction;
        private final List<NameValuePair> formInputs = new ArrayList();

        public void startElement(String namespaceURI, String localName, String qualifiedName, Attributes attributes)
                throws SAXException
        {
            this.elementHierarchy.add(qualifiedName);
            if (isElement(new String[] { "html", "body", "form" }))
            {
                if ("hiddenform".equals(attributes.getValue("name")))
                {
                    if (attributes.getValue("action") == null) {
                        throw new SAXException("ACS form lacks action");
                    }
                    this.inForm = true;
                    this.formAction = attributes.getValue("action");
                }
            }
            else if (this.inForm) {
                if (isElement(new String[] { "html", "body", "form", "input" }))
                {
                    if (!"hidden".equalsIgnoreCase(attributes.getValue("type"))) {
                        throw new SAXException(MessageFormat.format("Expected hidden input type in ACS result form, got: {0}", new Object[] { attributes.getValue("type") }));
                    }
                    if ((attributes.getValue("name") == null) || (attributes.getValue("value") == null)) {
                        throw new SAXException("Malformed input element - missing name / value pair");
                    }
                    this.formInputs.add(new NameValuePair(attributes.getValue("name"), attributes.getValue("value")));
                }
            }
        }

        public void endElement(String namespaceURI, String localName, String qualifiedName)
                throws SAXException
        {
            if (this.elementHierarchy.size() == 0) {
                throw new SAXException(MessageFormat.format("Malformed closing element: {0}", new Object[] { qualifiedName }));
            }
            if (!qualifiedName.equalsIgnoreCase((String)this.elementHierarchy.get(this.elementHierarchy.size() - 1))) {
                throw new SAXException(MessageFormat.format("Received unexpected closing element: {0}", new Object[] { qualifiedName }));
            }
            if (this.inForm) {
                if (isElement(new String[] { "html", "body", "form" })) {
                    this.inForm = false;
                }
            }
            this.elementHierarchy.remove(this.elementHierarchy.size() - 1);
        }

        public InputSource resolveEntity(String publicId, String systemId)
                throws SAXException
        {
            ACSCredentialsDialog.log.warn(MessageFormat.format("Refused to resolve entity {0} / {1}", new Object[] { publicId, systemId }));

            return null;
        }

        private boolean isElement(String... elements)
        {
            Check.notNull(elements, "elements");
            if (this.elementHierarchy.size() != elements.length) {
                return false;
            }
            for (int i = 0; i < elements.length; i++) {
                if (!elements[i].equalsIgnoreCase((String)this.elementHierarchy.get(i))) {
                    return false;
                }
            }
            return true;
        }

        public String getFormAction()
        {
            return this.formAction;
        }

        public NameValuePair[] getFormInputs()
        {
            return (NameValuePair[])this.formInputs.toArray(new NameValuePair[this.formInputs.size()]);
        }
    }
}
